<template>
    <!-- 能滚动的盒子 -->
    <div class="vitual-list" ref="vitualList">
        <!-- 自定义一个滚动条 -->
        <div class="scroll-bar" ref="scrollBar"></div>
        <!-- 真实渲染的内容 -->
        <div class="scroll-list" ref="scrollList" :style="{transform: `translate3d(0,${offset}px,0)`}">
            <div v-for="item in visibleData" :vid="item.id" :key="item.id" ref="itemsRef">
                <slot :item="item"></slot>
            </div>
        </div>
    </div>
</template>
<script lang="ts" setup>
import { ref, onMounted, computed, onUpdated, nextTick } from 'vue'
import { useScroll } from '@vueuse/core'
const props:any = defineProps({
    size: Number, // 每项的高度
    remain: Number, // 可见多少个
    variable: Boolean, // 是否需要动态计算每项的高度
    // 需要渲染的数据
    items: {
        type: Array as any,
        default: () => []
    }
})
const vitualList = ref<HTMLElement | null>(null)
const scrollBar = ref<HTMLElement | null>(null)
const scrollList = ref<HTMLElement | null>(null)
const itemsRef: any = ref()

const start = ref<number>(0)
const end = ref<number>(props.remain)
const offset = ref<number>(0)

// 前面预留几个
const prevCount = computed(() => {
    return Math.min(start.value, props.remain)
})
// 后面预留几个
const nextCount = computed(() => {
    return Math.min(props.items.length - end.value, props.remain)
})

// 可见的数据有哪些
const visibleData = computed(() => {
    // 会根据start 和 end 自动去截取渲染
    let _start = start.value - prevCount.value
    let _end = end.value + nextCount.value
     
    // 截取当前应该从第几个开始渲染，到第几个结束 
    return props.items.slice(_start, _end)
})

let positions:any[] = []
const cacheList = () => {
    // 缓存当前项的高度 和 top值 还有bottom
    positions = props.items.map((item, index) => ({
        height: props.size,
        top: index * props.size,
        bottom: (index + 1) * props.size
    }))
}
const getStartIndex = (scrollTop: number) => { // 查找当前滚动的需要找到的值
    
    let _start = 0 // 开始
    let _end = positions.length - 1 // 结束位置 
    let temp:any = null // 临时记录
    while(_start <= _end) {
        let midIndex = parseInt(((_start + _end) / 2) as any)
        // 找到当前中间那个数据的结尾点
        let midValue = positions[midIndex].bottom
        if(midValue === scrollTop) { // 如果直接找到了，就返回当前的下一个人
            return midIndex + 1
        } else if(midValue < scrollTop) { // 当前要查找的人 在右边
            _start = midIndex + 1
        } else if(midValue > scrollTop){ // 当前要查找的人 在左边的情况
            if(temp == null || temp > midIndex) {
                temp = midIndex // 找范围
            } 
            _end = midIndex - 1
        }    
    } 
    return temp
}
 
// 监听滚动事件
useScroll(vitualList,{
    // throttle: 200,
    onScroll: (el: Event) => {
        // 1、先算出来当前滚动过去几个了，当前应该从第几个开始显示
        let scrollTop: number = vitualList.value?.scrollTop || 0 // 获取当前滚动的距离

        if(props.variable){
            // 如果有传递variable 说明要使用二分查找找到对应的记录
            start.value = getStartIndex(scrollTop) 
            // 计算当前结尾的位置
            end.value = start.value + props.remain
            // 设置偏移量
            offset.value = positions[start.value - prevCount.value]?positions[start.value - prevCount.value].top : 0  // 让可视区域去调整偏移位置
 
        } else {
            // 2、获取当前应该从第几个开始渲染
            start.value = Math.floor(scrollTop / props.size)
            // 3、计算当前结尾的位置
            end.value = start.value + props.remain // 当前可渲染的区域
            
            // 定位当前的可视区域，让当前渲染的内容在当前的vitual-list的可视区域内
            // 如果有预留渲染，应该把这个位置在向上移动当前这么多
            offset.value = start.value * props.size - props.size * prevCount.value // 让可视区域去调整偏移位置
        }

    }
})



onMounted(() => {
    if(vitualList.value) {
        // 设置列表的高度
        vitualList.value.style.height = `${props.size * props.remain}px` 
    }
    if(scrollBar.value) {
        // 设置滚动条的高度
        scrollBar.value.style.height = `${props.size * props.items.length}px`
    } 
    
    // 如果加载完毕 需要缓存每一项的高度
    // 先记录好，等滚动的时候 去渲染页面时获取真实dom的高度 来更新缓存的内容
    cacheList()
    
    // 2.再重新计算滚动条的高度
})

onUpdated(() => {
    // 页面渲染完成后 需要根据当前展示的数据 更新缓存区的内容
    nextTick(() => {
        // 根据当前显示的 更新缓存中的 height bottom top ，最终更新滚动条的高度
        let nodes = itemsRef.value // 获取真实节点
        if(!(nodes&&nodes.length>0)) {
        return
        }
        nodes.forEach(node => {
        let { height } = node.getBoundingClientRect() // 真实的高度
        // 我希望更新老的高度
        let id = node.getAttribute('vid') - 0
        let oldHeight = positions[id]?.height || 0
        let val = oldHeight - height; // 计算当前的高度是否和之前的有变化
        if(val) {
            positions[id].height = height
            positions[id].bottom = positions[id].bottom - val // 底部增加了

            // 链表 将后续的所有人 都要向后移动
            for(let i = id + 1; i < positions.length; i++) {
                positions[i].top = positions[i-1].bottom // 更新top
                positions[i].bottom = positions[i].bottom - val // 更新bottom
            }  
        }  
        }); 
        // 更新滚动条的高度
        if(scrollBar.value) {
            scrollBar.value.style.height = `${positions[positions.length - 1].bottom}px`
        }
    })
})




</script>
<style lang="less" scoped>
.vitual-list{
    overflow-y: scroll;
    position: relative;
    .scroll-list{
        position: absolute;
        top: 0;
        left: 0;
        width: 100%;
    }
}
</style>